﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AdvUtils;
using AdvUtils.Regression;
using GeoUtils;

namespace GeoCoderParser
{
    class GeoEntityDatabase
    {
        DoubleArrayTrieSearch dartSearch_Address;
        List<List<GeoEntity>> vaAddressList;

        DoubleArrayTrieSearch dartSearch_TRStation;
        List<List<GeoEntity>> vaTRStationList;

        DoubleArrayTrieSearch dartSearch_Zone;
        List<List<GeoEntity>> vaZoneList;

        DoubleArrayTrieSearch dartSearch_GeoEntity;
        List<List<GeoEntity>> vaGeoEntityList;

        DoorplateExtractor doorplateExtractor;
        SingularPolynomialRegression spr;
                                
        public int SearchAddressByPrefix(string strPrefix, List<int> result)
        {
            return dartSearch_Address.SearchByPrefix(strPrefix, result);
        }

        public int SearchAddress(string strKey)
        {
            return dartSearch_Address.SearchByPerfectMatch(strKey);
        }

        public List<List<GeoEntity>> AddressList
        {
            get { return vaAddressList; }
        }

        public List<List<GeoEntity>> GeoEntityList
        {
            get { return vaGeoEntityList; }
        }


        public GeoEntityDatabase()
        {
            dartSearch_Address = null;
            vaAddressList = null;
            dartSearch_GeoEntity = null;
            vaGeoEntityList = null;
            dartSearch_TRStation = null;
            vaTRStationList = null;
            dartSearch_Zone = null;
            vaZoneList = null;
        }

        public void Initialize(string strDataRoot)
        {
            //Load Address data
            LoadGeoData(strDataRoot + "address", out dartSearch_Address, out vaAddressList);

            //Load TRStation data
            LoadGeoData(strDataRoot + "trstation", out dartSearch_TRStation, out vaTRStationList);

            //Load Zone data
            LoadGeoData(strDataRoot + "zone", out dartSearch_Zone, out vaZoneList);

            //Load Geo entity data
            LoadGeoData(strDataRoot + "geoentity", out dartSearch_GeoEntity, out vaGeoEntityList);

            doorplateExtractor = new DoorplateExtractor();
            spr = new SingularPolynomialRegression();
        }


        //Load geo-items data from file
        private bool LoadGeoData(string strLocationName, out DoubleArrayTrieSearch dartSearch, out List<List<GeoEntity>> vaList)
        {
            dartSearch = new DoubleArrayTrieSearch();
            dartSearch.Load(strLocationName + ".dart");

            GeoEntityStorage geoItemStorage = new GeoEntityStorage();
            vaList = geoItemStorage.LoadFromFile(strLocationName + ".dat");

            return true;
        }


        public LatLong RegressAddressLineLatlong(List<GeoEntity> geoEntityList, string strAddressHost, Doorplate doorplate)
        {
            //"Addresshost + doorplate" in the same admin code
            SortedDictionary<int, List<GeoEntity>> sdict = new SortedDictionary<int, List<GeoEntity>>();
            List<double> XList = new List<double>();
            List<double> YList = new List<double>();

            //The validated (X,Y) range
            double fMinX = 360.0, fMaxX = -360.0;
            double fMinY = 360.0, fMaxY = -360.0;

            foreach (GeoEntity geoEntity in geoEntityList)
            {
                string strDP = geoEntity.Name.Replace(strAddressHost, "");
                Doorplate dp = doorplateExtractor.Extract(strDP);
                if (dp == null)
                {
                    continue;
                }

                int distance = Math.Abs(dp.DistanceTo(doorplate));
                if (sdict.ContainsKey(distance) == false)
                {
                    sdict.Add(distance, new List<GeoEntity>());
                }
                sdict[distance].Add(geoEntity);

                XList.Add(geoEntity.LatLong.Longitude);
                YList.Add(geoEntity.LatLong.Latitude);

                if (geoEntity.LatLong.Longitude < fMinX)
                {
                    fMinX = geoEntity.LatLong.Longitude;
                }
                if (geoEntity.LatLong.Longitude > fMaxX)
                {
                    fMaxX = geoEntity.LatLong.Longitude;
                }
                if (geoEntity.LatLong.Latitude < fMinY)
                {
                    fMinY = geoEntity.LatLong.Latitude;
                }
                if (geoEntity.LatLong.Latitude > fMaxY)
                {
                    fMaxY = geoEntity.LatLong.Latitude;
                }
            }

            if (sdict.Count == 0)
            {
                return null;
            }

            //Select two geo-entities which are the neareast to the given address line
            GeoEntity ge_a = null, ge_b = null;
            int distance_a = 0, distance_b = 0;
            foreach (KeyValuePair<int, List<GeoEntity>> spair in sdict)
            {
                if (ge_a == null)
                {
                    ge_a = spair.Value[0];

                    string strDP = ge_a.Name.Replace(strAddressHost, "");
                    Doorplate dp = doorplateExtractor.Extract(strDP);
                    distance_a = dp.DistanceTo(doorplate);
                }

                foreach (GeoEntity item in spair.Value)
                {
                    if (item != ge_a && item.LatLong.Longitude - ge_a.LatLong.Longitude != 0)
                    {
                        ge_b = item;
                        string strDP = ge_b.Name.Replace(strAddressHost, "");
                        Doorplate dp = doorplateExtractor.Extract(strDP);
                        distance_b = dp.DistanceTo(doorplate);
                        break;
                    }
                }

                if (ge_b != null)
                {
                    break;
                }
            }
            if (ge_a == null)
            {
                return null;
            }

            if (ge_a != null && ge_b != null)
            {
                //x = (a - d * b) / (1 - d)
                double distance_rate = (double)distance_a / (double)distance_b;
                double x, y;

                x = (ge_a.LatLong.Longitude - distance_rate * ge_b.LatLong.Longitude) / (1.0 - distance_rate);
                //If either x or regressed y is invalidated, assign the neareast geo-entity location as fuzzy matched result
                if (x < fMinX || x > fMaxX)
                {
                    return ge_a.LatLong;
                }
                //Regression the quadratic function to describ the road
                spr.Regress(XList.ToArray(), YList.ToArray(), 2);
                y = spr.Coefficients[2] * x * x + spr.Coefficients[1] * x + spr.Coefficients[0];
                if (y < fMinY || y > fMaxY)
                {
                    return ge_a.LatLong;
                }
                return new LatLong(y, x);
            }

            return ge_a.LatLong;
        }

        //Get combined geo-entity
        public void GetCandidateCombinedGeoEntity(Token tk, WordSeg.Tokens tokens, int i)
        {
            //Check whether current token is labeled as LOC_ADDR and previous token is labeled as LOC_ROD by statistics model
            if (tk.strTagList.Contains("LOC_ADDR") == true)
            {
                if (i > 0 && tokens.tokenList[i - 1].strTagList.Contains("LOC_ROD") == true)
                {
                    //Found a term labeled as road
                    string strAddressHost = tokens.tokenList[i - 1].strTerm;
                    string strDoorplate = tk.strTerm;
                    string strMergedName = strAddressHost + strDoorplate;
                    int offset;
                    offset = SearchAddress(strMergedName);
                    if (offset >= 0)
                    {
                        //Found address in database
                        tk.candGeoEntityList.AddRange(AddressList[offset]);
                    }
                    else
                    {
                        //Not found, use fuzzy match
                        Doorplate doorplate = doorplateExtractor.Extract(strDoorplate);
                        if (doorplate == null)
                        {
                            //Can not extract doorplate
                            return;
                        }

                        List<int> addrOffsetList = new List<int>();
                        int cnt = SearchAddressByPrefix(strAddressHost, addrOffsetList);

                        List<GeoEntity> addrGeoEntityList = new List<GeoEntity>();
                        foreach (int item in addrOffsetList)
                        {
                            addrGeoEntityList.AddRange(AddressList[item]);
                        }

                        //The same "Address Host+Doorplate" name may belong to more than one admin code.
                        //So we make groups of them by their admin code, and make regression
                        Dictionary<int, List<GeoEntity>> adminCode2GeoEntityList = new Dictionary<int, List<GeoEntity>>();
                        foreach (GeoEntity item in addrGeoEntityList)
                        {
                            if (item.Tag == "DICT_LOC_ADDR")
                            {
                                //We only use indexed address line as sampled data
                                if (adminCode2GeoEntityList.ContainsKey(item.AdminCode) == false)
                                {
                                    adminCode2GeoEntityList.Add(item.AdminCode, new List<GeoEntity>());
                                }
                                adminCode2GeoEntityList[item.AdminCode].Add(item);
                            }
                        }

                        foreach (KeyValuePair<int, List<GeoEntity>> pair in adminCode2GeoEntityList)
                        {
                            //Regress address line's lat/long
                            LatLong latlong = RegressAddressLineLatlong(pair.Value, strAddressHost, doorplate);
                            if (latlong != null)
                            {
                                //Assign the first geo-entity's admin code, parent admin code, level to the new geo-entity
                                GeoEntity existedGeoEntity = pair.Value[0];
                                GeoEntity ge = new GeoEntity(strMergedName, "DICT_LOC_ADDR_FUZZY_MATCH", false, 
                                    existedGeoEntity.AdminCodeList, existedGeoEntity.AdminPreCode, existedGeoEntity.iLevel, latlong, null);
                                tk.candGeoEntityList.Add(ge);
                            }
                        }
                    }
                }
            }
        }


        //Get all single token candidate geo-entities
        //Token : current token
        //minLevel : the min level in geo-entity
        public void GetCandidateSingleGeoEntity(Token token, int minLevel = 10, bool bOnlyNoRewritten = false)
        {
            int offset;

            if (dartSearch_GeoEntity != null)
            {
                //retrieval geo-entities from indexed geo entity data by token's term.
                offset = dartSearch_GeoEntity.SearchByPerfectMatch(token.strTerm);
                if (offset >= 0)
                {
                    //If found geo-entities's tag is in token's tag list, add the entity into candidate list
                    List<GeoEntity> geoEntityList = vaGeoEntityList[offset];
                    foreach (GeoEntity item in geoEntityList)
                    {
                        if (token.strTagList.Contains(item.Tag) == true)
                        {
                            token.candGeoEntityList.Add(item);
                        }
                    }
                }
            }

            if (dartSearch_Address != null)
            {
                if (token.strTagList.Contains("DICT_LOC_ADDR") == true)
                {
                    offset = dartSearch_Address.SearchByPerfectMatch(token.strTerm);
                    if (offset >= 0)
                    {
                        token.candGeoEntityList.AddRange(vaAddressList[offset]);
                    }
                }
            }

            if (dartSearch_TRStation != null)
            {
                if (token.strTagList.Contains("DICT_LOC_TRStation") == true)
                {
                    offset = dartSearch_TRStation.SearchByPerfectMatch(token.strTerm);
                    if (offset >= 0)
                    {
                        token.candGeoEntityList.AddRange(vaTRStationList[offset]);
                    }
                }
            }

            if (dartSearch_Zone != null)
            {
                if (token.strTagList.Contains("DICT_LOC_ZONE") == true)
                {
                    offset = dartSearch_Zone.SearchByPerfectMatch(token.strTerm);
                    if (offset >= 0)
                    {
                        token.candGeoEntityList.AddRange(vaZoneList[offset]);
                    }
                }
            }

            List<GeoEntity> candGeoList = new List<GeoEntity>();
            foreach (GeoEntity item in token.candGeoEntityList)
            {
                if (item.iLevel < minLevel)
                {
                    if (bOnlyNoRewritten == false ||
                        (bOnlyNoRewritten == true && item.bRewritten == false))
                    {
                        candGeoList.Add(item);
                    }
                }
            }
            token.candGeoEntityList = candGeoList;
        }

    }
}
